Skip to content

netstack: allow defaultHandler respond ICMPv4Echo in promiscuous mode #11609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2025

Conversation

Amaindex
Copy link
Contributor

@Amaindex Amaindex commented Apr 2, 2025

Motivation

The gVisor network stack is extensively employed in user-space tunneling software, often operating in promiscuous mode. In this configuration, the stack directly responds to all ICMPv4 Echo Request packets, irrespective of whether a transport defaultHandler has already processed them. This behavior is often unintended in certain scenarios, as evidenced by issues such as:

In these scenarios, users may prefer to utilize SetTransportProtocolHandler to configure a custom defaultHandler for tailored processing of ICMPv4 Echo packets.

In issue #8657, @kevinGC proposed a potential solution:

// If a customer ICMPv4 protocol handler has been set, use that in favor of default handling.
if p == header.ICMPv4ProtocolNumber {
    if _, ok := e.protocol.stack.transportProtocols[header.ICMPv4ProtocolNumber]; !ok {
        // handle the "normal" way
    }
}

However, this approach appears somewhat aggressive, as it could impair applications that depend on the existing gVisor stack behavior with ICMPv4 protocol handlers, such as runsc itself. These programs would require code adjustments to accommodate this change, as shown below:

// gvisor/runsc/boot/loader.go
func newEmptySandboxNetworkStack(clock tcpip.Clock, allowPacketEndpointWrite bool) (*netstack.Stack, error) {
    netProtos := []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol, arp.NewProtocol}
    transProtos := []stack.TransportProtocolFactory{
        tcp.NewProtocol,
        udp.NewProtocol,
        // icmp.NewProtocol4, runsc would need to remove this to allow stack ICMPv4Echo replies.
        icmp.NewProtocol6,
    }
    // ...
}

This patch introduces an alternative by enabling the stack to refrain from directly responding to ICMPv4 Echo packets delivered locally due to promiscuous mode, thereby allowing the defaultHandler to handle them independently.

This proposal is presented as an initial step for discussion, and insights from experts on potential refinements or superior alternatives are warmly welcomed.

Testing and adjustments for ICMPv6 will be addressed once the approach is finalized.

Patch Details

In ipv4.go:handleValidatedPacket, the packet is evaluated based on AcquireAssignedAddress to determine local delivery or forwarding:

func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}

In promiscuous mode, packets destined for unknown addresses are assigned a temporary address and delivered locally:

/*
handleValidatedPacket
 \----AcquireAssignedAddress
       \----AcquireAssignedAddressOrMatching
             \----addAndAcquireAddressLocked
*/
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB}, Temporary)
    // ...
}

By leveraging the Temporary field in AddressProperties, we can identify packets delivered locally due to promiscuous mode. A new field, LocalAddressTemporary, is added to NetworkPacketInfo to record this status:

// pkg/tcpip/stack/addressable_endpoint_state.go
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB, Temporary: true}, Temporary) // set AddressProperties.Temporary
    // ...
}

// pkg/tcpip/network/ipv4/ipv4.go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        pkt.NetworkPacketInfo.LocalAddressTemporary = addressEndpoint.Temporary() // packets delivered locally due to promiscuous mode 
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}

Finally, in icmp.go:handleICMP, direct replies to such packets are skipped:

func (e *endpoint) handleICMP(pkt *stack.PacketBuffer) {
    // ...
    switch h.Type() {
    case header.ICMPv4Echo:
        // ...
        localAddressTemporary := pkt.NetworkPacketInfo.LocalAddressTemporary
        localAddressBroadcast := pkt.NetworkPacketInfo.LocalAddressBroadcast

        // It's possible that a raw socket or custom defaultHandler expects to
	// receive this packet.
        e.dispatcher.DeliverTransportPacket(header.ICMPv4ProtocolNumber, pkt)
        pkt = nil

        // Skip direct ICMP echo reply if the packet was received with a temporary
	// address, allowing custom handlers to take over.
        if localAddressTemporary {
            return
        }
        // ...
    }
}

Quick Testing

To validate this patch, a simple test program and procedure are provided below:

package main

import (
    "fmt"
    "net"
    "gvisor.dev/gvisor/pkg/tcpip"
    "gvisor.dev/gvisor/pkg/tcpip/header"
    "gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
    "gvisor.dev/gvisor/pkg/tcpip/link/tun"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
    "gvisor.dev/gvisor/pkg/tcpip/stack"
    "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)

func main() {
    fd, _ := tun.Open("tun0")
    ep, _ := fdbased.New(&fdbased.Options{
        FDs:                   []int{fd},
        MTU:                   1500,
        EthernetHeader:        false,
        PacketDispatchMode:    fdbased.Readv,
        MaxSyscallHeaderBytes: 0x00,
    })
    s := stack.New(stack.Options{
        NetworkProtocols: []stack.NetworkProtocolFactory{
            ipv4.NewProtocol,
            ipv6.NewProtocol,
        },
        TransportProtocols: []stack.TransportProtocolFactory{
            tcp.NewProtocol,
            udp.NewProtocol,
            icmp.NewProtocol4,
            icmp.NewProtocol6,
        },
    })

    nicID := s.NextNICID()
    s.CreateNICWithOptions(nicID, ep,
        stack.NICOptions{
            Disabled: false,
            QDisc:    nil,
        },
    )
    s.SetPromiscuousMode(nicID, true)
    s.SetSpoofing(nicID, true)
    s.SetRouteTable([]tcpip.Route{
        {Destination: header.IPv4EmptySubnet, NIC: nicID},
        {Destination: header.IPv6EmptySubnet, NIC: nicID},
    })
    s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
        h := header.ICMPv4(pkt.TransportHeader().Slice())
        fmt.Printf("icmpv4: %s->%s, hType: %v, ", id.RemoteAddress, id.LocalAddress, h.Type())
        if !pkt.NetworkPacketInfo.LocalAddressTemporary {
            fmt.Println("packet to permanent address, processed by stack")
            return true
        }
        fmt.Println("packet to temporary address, need to process by user")
        return true
    })
    protocolAddr := tcpip.ProtocolAddress{
        Protocol: ipv4.ProtocolNumber,
        AddressWithPrefix: tcpip.AddressWithPrefix{
            Address:   tcpip.AddrFromSlice(net.IPv4(11, 0, 0, 1).To4()),
            PrefixLen: 8,
        },
    }
    s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint, Temporary: false})

    fmt.Println("stack started ...")
    select {}
}

Test procedure:

$ ip tuntap add mode tun dev tun0
ip link set dev tun0 up 

$ go run main.go  
stack started ...

icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack

icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
$ ping 11.0.0.1
PING 11.0.0.1 (11.0.0.1) 56(84) bytes of data.
64 bytes from 11.0.0.1: icmp_seq=1 ttl=64 time=0.441 ms
64 bytes from 11.0.0.1: icmp_seq=2 ttl=64 time=0.425 ms
64 bytes from 11.0.0.1: icmp_seq=3 ttl=64 time=0.437 ms
^C
--- 11.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2012ms
rtt min/avg/max/mdev = 0.425/0.434/0.441/0.006 ms

$ ping 11.0.0.2
PING 11.0.0.2 (11.0.0.2) 56(84) bytes of data.
^C
--- 11.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2052ms

In promiscuous mode, ICMPv4Echo packets are replied to directly by the
network stack, even if custom transport defaultHandler processes them.
This change adds a LocalAddressTemporary field to NetworkPacketInfo to
identify packets received with temporary addresses due to promiscuous
mode, and skips the direct ICMP reply for these. This allows custom
handlers to independently process such packets.

- Added LocalAddressTemporary field to NetworkPacketInfo.
- Set Temporary property when adding temporary address.
- Set LocalAddressTemporary in addressEndpoint check.
- Skip direct ICMPv4Echo reply for packets with temporary addresses.

For google#8657
Copy link

google-cla bot commented Apr 2, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@nybidari nybidari self-requested a review April 8, 2025 16:57
@avagin avagin added area: networking Issue related to networking ready to pull labels Apr 24, 2025
copybara-service bot pushed a commit that referenced this pull request Apr 30, 2025
## Motivation

The gVisor network stack is extensively employed in user-space tunneling software, often operating in promiscuous mode. In this configuration, the stack directly responds to all ICMPv4 Echo Request packets, irrespective of whether a [transport defaultHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L49) has already processed them. This behavior is often unintended in certain scenarios, as evidenced by issues such as:

- #8657
- containers/gvisor-tap-vsock#428
- xjasonlyu/tun2socks#361
- etc.

In these scenarios, users may prefer to utilize [SetTransportProtocolHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L517) to configure a custom `defaultHandler` for tailored processing of ICMPv4 Echo packets.

In issue #8657, @kevinGC proposed a potential solution:

```go
// If a customer ICMPv4 protocol handler has been set, use that in favor of default handling.
if p == header.ICMPv4ProtocolNumber {
    if _, ok := e.protocol.stack.transportProtocols[header.ICMPv4ProtocolNumber]; !ok {
        // handle the "normal" way
    }
}
```

However, this approach appears somewhat aggressive, as it could impair applications that depend on the existing gVisor stack behavior with ICMPv4 protocol handlers, such as [runsc itself](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/runsc/boot/loader.go#L1536). These programs would require code adjustments to accommodate this change, as shown below:

```go
// gvisor/runsc/boot/loader.go
func newEmptySandboxNetworkStack(clock tcpip.Clock, allowPacketEndpointWrite bool) (*netstack.Stack, error) {
    netProtos := []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol, arp.NewProtocol}
    transProtos := []stack.TransportProtocolFactory{
        tcp.NewProtocol,
        udp.NewProtocol,
        // icmp.NewProtocol4, runsc would need to remove this to allow stack ICMPv4Echo replies.
        icmp.NewProtocol6,
    }
    // ...
}
```

This patch introduces an alternative by enabling the stack to refrain from directly responding to ICMPv4 Echo packets delivered locally due to promiscuous mode, thereby allowing the defaultHandler to [handle](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/nic.go#L873) them independently.

**This proposal is presented as an initial step for discussion, and insights from experts on potential refinements or superior alternatives are warmly welcomed.**

**Testing and adjustments for ICMPv6 will be addressed once the approach is finalized.**

## Patch Details

In [ipv4.go:handleValidatedPacket](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/ipv4.go#L1108), the packet is evaluated based on `AcquireAssignedAddress` to determine local delivery or forwarding:

```go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

In promiscuous mode, packets destined for unknown addresses are assigned a temporary address and delivered locally:

```go
/*
handleValidatedPacket
 \----AcquireAssignedAddress
       \----AcquireAssignedAddressOrMatching
             \----addAndAcquireAddressLocked
*/
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB}, Temporary)
    // ...
}
```

By leveraging the `Temporary` field in [AddressProperties](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/registration.go#L492), we can identify packets delivered locally due to promiscuous mode. A new field, `LocalAddressTemporary`, is added to `NetworkPacketInfo` to record this status:

```go
// pkg/tcpip/stack/addressable_endpoint_state.go
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB, Temporary: true}, Temporary) // set AddressProperties.Temporary
    // ...
}

// pkg/tcpip/network/ipv4/ipv4.go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        pkt.NetworkPacketInfo.LocalAddressTemporary = addressEndpoint.Temporary() // packets delivered locally due to promiscuous mode
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

Finally, in [icmp.go:handleICMP](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/icmp.go#L282), direct replies to such packets are skipped:

```go
func (e *endpoint) handleICMP(pkt *stack.PacketBuffer) {
    // ...
    switch h.Type() {
    case header.ICMPv4Echo:
        // ...
        localAddressTemporary := pkt.NetworkPacketInfo.LocalAddressTemporary
        localAddressBroadcast := pkt.NetworkPacketInfo.LocalAddressBroadcast

        // It's possible that a raw socket or custom defaultHandler expects to
	// receive this packet.
        e.dispatcher.DeliverTransportPacket(header.ICMPv4ProtocolNumber, pkt)
        pkt = nil

        // Skip direct ICMP echo reply if the packet was received with a temporary
	// address, allowing custom handlers to take over.
        if localAddressTemporary {
            return
        }
        // ...
    }
}
```

## Quick Testing

To validate this patch, a simple test program and procedure are provided below:

```go
package main

import (
    "fmt"
    "net"
    "gvisor.dev/gvisor/pkg/tcpip"
    "gvisor.dev/gvisor/pkg/tcpip/header"
    "gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
    "gvisor.dev/gvisor/pkg/tcpip/link/tun"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
    "gvisor.dev/gvisor/pkg/tcpip/stack"
    "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)

func main() {
    fd, _ := tun.Open("tun0")
    ep, _ := fdbased.New(&fdbased.Options{
        FDs:                   []int{fd},
        MTU:                   1500,
        EthernetHeader:        false,
        PacketDispatchMode:    fdbased.Readv,
        MaxSyscallHeaderBytes: 0x00,
    })
    s := stack.New(stack.Options{
        NetworkProtocols: []stack.NetworkProtocolFactory{
            ipv4.NewProtocol,
            ipv6.NewProtocol,
        },
        TransportProtocols: []stack.TransportProtocolFactory{
            tcp.NewProtocol,
            udp.NewProtocol,
            icmp.NewProtocol4,
            icmp.NewProtocol6,
        },
    })

    nicID := s.NextNICID()
    s.CreateNICWithOptions(nicID, ep,
        stack.NICOptions{
            Disabled: false,
            QDisc:    nil,
        },
    )
    s.SetPromiscuousMode(nicID, true)
    s.SetSpoofing(nicID, true)
    s.SetRouteTable([]tcpip.Route{
        {Destination: header.IPv4EmptySubnet, NIC: nicID},
        {Destination: header.IPv6EmptySubnet, NIC: nicID},
    })
    s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
        h := header.ICMPv4(pkt.TransportHeader().Slice())
        fmt.Printf("icmpv4: %s->%s, hType: %v, ", id.RemoteAddress, id.LocalAddress, h.Type())
        if !pkt.NetworkPacketInfo.LocalAddressTemporary {
            fmt.Println("packet to permanent address, processed by stack")
            return true
        }
        fmt.Println("packet to temporary address, need to process by user")
        return true
    })
    protocolAddr := tcpip.ProtocolAddress{
        Protocol: ipv4.ProtocolNumber,
        AddressWithPrefix: tcpip.AddressWithPrefix{
            Address:   tcpip.AddrFromSlice(net.IPv4(11, 0, 0, 1).To4()),
            PrefixLen: 8,
        },
    }
    s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint, Temporary: false})

    fmt.Println("stack started ...")
    select {}
}
```

Test procedure:

```shell
$ ip tuntap add mode tun dev tun0
ip link set dev tun0 up

$ go run main.go
stack started ...

icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack

icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user

```

```shell
$ ping 11.0.0.1
PING 11.0.0.1 (11.0.0.1) 56(84) bytes of data.
64 bytes from 11.0.0.1: icmp_seq=1 ttl=64 time=0.441 ms
64 bytes from 11.0.0.1: icmp_seq=2 ttl=64 time=0.425 ms
64 bytes from 11.0.0.1: icmp_seq=3 ttl=64 time=0.437 ms
^C
--- 11.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2012ms
rtt min/avg/max/mdev = 0.425/0.434/0.441/0.006 ms

$ ping 11.0.0.2
PING 11.0.0.2 (11.0.0.2) 56(84) bytes of data.
^C
--- 11.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2052ms
```

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11609 from Amaindex:master 868dfbc
PiperOrigin-RevId: 753217427
copybara-service bot pushed a commit that referenced this pull request Apr 30, 2025
## Motivation

The gVisor network stack is extensively employed in user-space tunneling software, often operating in promiscuous mode. In this configuration, the stack directly responds to all ICMPv4 Echo Request packets, irrespective of whether a [transport defaultHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L49) has already processed them. This behavior is often unintended in certain scenarios, as evidenced by issues such as:

- #8657
- containers/gvisor-tap-vsock#428
- xjasonlyu/tun2socks#361
- etc.

In these scenarios, users may prefer to utilize [SetTransportProtocolHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L517) to configure a custom `defaultHandler` for tailored processing of ICMPv4 Echo packets.

In issue #8657, @kevinGC proposed a potential solution:

```go
// If a customer ICMPv4 protocol handler has been set, use that in favor of default handling.
if p == header.ICMPv4ProtocolNumber {
    if _, ok := e.protocol.stack.transportProtocols[header.ICMPv4ProtocolNumber]; !ok {
        // handle the "normal" way
    }
}
```

However, this approach appears somewhat aggressive, as it could impair applications that depend on the existing gVisor stack behavior with ICMPv4 protocol handlers, such as [runsc itself](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/runsc/boot/loader.go#L1536). These programs would require code adjustments to accommodate this change, as shown below:

```go
// gvisor/runsc/boot/loader.go
func newEmptySandboxNetworkStack(clock tcpip.Clock, allowPacketEndpointWrite bool) (*netstack.Stack, error) {
    netProtos := []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol, arp.NewProtocol}
    transProtos := []stack.TransportProtocolFactory{
        tcp.NewProtocol,
        udp.NewProtocol,
        // icmp.NewProtocol4, runsc would need to remove this to allow stack ICMPv4Echo replies.
        icmp.NewProtocol6,
    }
    // ...
}
```

This patch introduces an alternative by enabling the stack to refrain from directly responding to ICMPv4 Echo packets delivered locally due to promiscuous mode, thereby allowing the defaultHandler to [handle](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/nic.go#L873) them independently.

**This proposal is presented as an initial step for discussion, and insights from experts on potential refinements or superior alternatives are warmly welcomed.**

**Testing and adjustments for ICMPv6 will be addressed once the approach is finalized.**

## Patch Details

In [ipv4.go:handleValidatedPacket](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/ipv4.go#L1108), the packet is evaluated based on `AcquireAssignedAddress` to determine local delivery or forwarding:

```go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

In promiscuous mode, packets destined for unknown addresses are assigned a temporary address and delivered locally:

```go
/*
handleValidatedPacket
 \----AcquireAssignedAddress
       \----AcquireAssignedAddressOrMatching
             \----addAndAcquireAddressLocked
*/
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB}, Temporary)
    // ...
}
```

By leveraging the `Temporary` field in [AddressProperties](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/registration.go#L492), we can identify packets delivered locally due to promiscuous mode. A new field, `LocalAddressTemporary`, is added to `NetworkPacketInfo` to record this status:

```go
// pkg/tcpip/stack/addressable_endpoint_state.go
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB, Temporary: true}, Temporary) // set AddressProperties.Temporary
    // ...
}

// pkg/tcpip/network/ipv4/ipv4.go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        pkt.NetworkPacketInfo.LocalAddressTemporary = addressEndpoint.Temporary() // packets delivered locally due to promiscuous mode
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

Finally, in [icmp.go:handleICMP](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/icmp.go#L282), direct replies to such packets are skipped:

```go
func (e *endpoint) handleICMP(pkt *stack.PacketBuffer) {
    // ...
    switch h.Type() {
    case header.ICMPv4Echo:
        // ...
        localAddressTemporary := pkt.NetworkPacketInfo.LocalAddressTemporary
        localAddressBroadcast := pkt.NetworkPacketInfo.LocalAddressBroadcast

        // It's possible that a raw socket or custom defaultHandler expects to
	// receive this packet.
        e.dispatcher.DeliverTransportPacket(header.ICMPv4ProtocolNumber, pkt)
        pkt = nil

        // Skip direct ICMP echo reply if the packet was received with a temporary
	// address, allowing custom handlers to take over.
        if localAddressTemporary {
            return
        }
        // ...
    }
}
```

## Quick Testing

To validate this patch, a simple test program and procedure are provided below:

```go
package main

import (
    "fmt"
    "net"
    "gvisor.dev/gvisor/pkg/tcpip"
    "gvisor.dev/gvisor/pkg/tcpip/header"
    "gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
    "gvisor.dev/gvisor/pkg/tcpip/link/tun"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
    "gvisor.dev/gvisor/pkg/tcpip/stack"
    "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)

func main() {
    fd, _ := tun.Open("tun0")
    ep, _ := fdbased.New(&fdbased.Options{
        FDs:                   []int{fd},
        MTU:                   1500,
        EthernetHeader:        false,
        PacketDispatchMode:    fdbased.Readv,
        MaxSyscallHeaderBytes: 0x00,
    })
    s := stack.New(stack.Options{
        NetworkProtocols: []stack.NetworkProtocolFactory{
            ipv4.NewProtocol,
            ipv6.NewProtocol,
        },
        TransportProtocols: []stack.TransportProtocolFactory{
            tcp.NewProtocol,
            udp.NewProtocol,
            icmp.NewProtocol4,
            icmp.NewProtocol6,
        },
    })

    nicID := s.NextNICID()
    s.CreateNICWithOptions(nicID, ep,
        stack.NICOptions{
            Disabled: false,
            QDisc:    nil,
        },
    )
    s.SetPromiscuousMode(nicID, true)
    s.SetSpoofing(nicID, true)
    s.SetRouteTable([]tcpip.Route{
        {Destination: header.IPv4EmptySubnet, NIC: nicID},
        {Destination: header.IPv6EmptySubnet, NIC: nicID},
    })
    s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
        h := header.ICMPv4(pkt.TransportHeader().Slice())
        fmt.Printf("icmpv4: %s->%s, hType: %v, ", id.RemoteAddress, id.LocalAddress, h.Type())
        if !pkt.NetworkPacketInfo.LocalAddressTemporary {
            fmt.Println("packet to permanent address, processed by stack")
            return true
        }
        fmt.Println("packet to temporary address, need to process by user")
        return true
    })
    protocolAddr := tcpip.ProtocolAddress{
        Protocol: ipv4.ProtocolNumber,
        AddressWithPrefix: tcpip.AddressWithPrefix{
            Address:   tcpip.AddrFromSlice(net.IPv4(11, 0, 0, 1).To4()),
            PrefixLen: 8,
        },
    }
    s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint, Temporary: false})

    fmt.Println("stack started ...")
    select {}
}
```

Test procedure:

```shell
$ ip tuntap add mode tun dev tun0
ip link set dev tun0 up

$ go run main.go
stack started ...

icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack

icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user

```

```shell
$ ping 11.0.0.1
PING 11.0.0.1 (11.0.0.1) 56(84) bytes of data.
64 bytes from 11.0.0.1: icmp_seq=1 ttl=64 time=0.441 ms
64 bytes from 11.0.0.1: icmp_seq=2 ttl=64 time=0.425 ms
64 bytes from 11.0.0.1: icmp_seq=3 ttl=64 time=0.437 ms
^C
--- 11.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2012ms
rtt min/avg/max/mdev = 0.425/0.434/0.441/0.006 ms

$ ping 11.0.0.2
PING 11.0.0.2 (11.0.0.2) 56(84) bytes of data.
^C
--- 11.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2052ms
```

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11609 from Amaindex:master 868dfbc
PiperOrigin-RevId: 753217427
@ayushr2
Copy link
Collaborator

ayushr2 commented Apr 30, 2025

@Amaindex I was trying to submit this change, but seems like this change causes one of our PacketImpact tests to fail:

--- FAIL: TestTCPSynCookie/flags=F___A___ (2.22s)
--
  | --- FAIL: TestTCPSynCookie/flags=F___A___/with_syncookies (1.03s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:41575 DstPort:36289 SeqNum:3921319953 AckNum:670126177 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]
  | --- FAIL: TestTCPSynCookie/flags=____A___ (2.25s)
  | --- FAIL: TestTCPSynCookie/flags=____A___/with_syncookies (1.03s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:54807 DstPort:49265 SeqNum:456674521 AckNum:2572663956 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]
  | --- FAIL: TestTCPSynCookie/flags=___PA___ (2.26s)
  | --- FAIL: TestTCPSynCookie/flags=___PA___/with_syncookies (1.02s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:41113 DstPort:41799 SeqNum:462015974 AckNum:365526401 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]
  | --- FAIL: TestTCPSynCookie/flags=__R_____ (4.19s)
  | --- FAIL: TestTCPSynCookie/flags=__R_____/with_syncookies (1.03s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:43677 DstPort:39221 SeqNum:960333397 AckNum:704123591 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]
  | --- FAIL: TestTCPSynCookie/flags=__R_A___ (4.23s)
  | --- FAIL: TestTCPSynCookie/flags=__R_A___/with_syncookies (1.03s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:43655 DstPort:37155 SeqNum:2587297804 AckNum:2588211101 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]
  | --- FAIL: TestTCPSynCookie/flags=_S__A___ (4.23s)
  | --- FAIL: TestTCPSynCookie/flags=_S__A___/with_syncookies (1.03s)
  | tcp_syncookie_test.go:99: expected no retransmitted SYNACK, but got [&testbench.Ether{SrcAddr:de:67:e3:64:b6:ac DstAddr:3e:13:1e:e9:12:ce Type:2048} &testbench.IPv4{IHL:20 TOS:0 TotalLength:44 ID:0 Flags:2 FragmentOffset:0 TTL:64 Protocol:6 Checksum:58056 SrcAddr:172.0.0.2 DstAddr:172.0.0.1 Options:
  | } &testbench.TCP{SrcPort:52429 DstPort:48657 SeqNum:4289619898 AckNum:3424044991 DataOffset:24 Flags: S  A    WindowSize:64240 Checksum:22562 UrgentPointer:0 Options:
  | 00000000  02 04 05 b4                                       \|....\|
  | } &testbench.Payload{Bytes:
  | }]

See https://buildkite.com/gvisor/pipeline/builds/35744.

copybara-service bot pushed a commit that referenced this pull request Apr 30, 2025
## Motivation

The gVisor network stack is extensively employed in user-space tunneling software, often operating in promiscuous mode. In this configuration, the stack directly responds to all ICMPv4 Echo Request packets, irrespective of whether a [transport defaultHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L49) has already processed them. This behavior is often unintended in certain scenarios, as evidenced by issues such as:

- #8657
- containers/gvisor-tap-vsock#428
- xjasonlyu/tun2socks#361
- etc.

In these scenarios, users may prefer to utilize [SetTransportProtocolHandler](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/stack.go#L517) to configure a custom `defaultHandler` for tailored processing of ICMPv4 Echo packets.

In issue #8657, @kevinGC proposed a potential solution:

```go
// If a customer ICMPv4 protocol handler has been set, use that in favor of default handling.
if p == header.ICMPv4ProtocolNumber {
    if _, ok := e.protocol.stack.transportProtocols[header.ICMPv4ProtocolNumber]; !ok {
        // handle the "normal" way
    }
}
```

However, this approach appears somewhat aggressive, as it could impair applications that depend on the existing gVisor stack behavior with ICMPv4 protocol handlers, such as [runsc itself](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/runsc/boot/loader.go#L1536). These programs would require code adjustments to accommodate this change, as shown below:

```go
// gvisor/runsc/boot/loader.go
func newEmptySandboxNetworkStack(clock tcpip.Clock, allowPacketEndpointWrite bool) (*netstack.Stack, error) {
    netProtos := []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol, arp.NewProtocol}
    transProtos := []stack.TransportProtocolFactory{
        tcp.NewProtocol,
        udp.NewProtocol,
        // icmp.NewProtocol4, runsc would need to remove this to allow stack ICMPv4Echo replies.
        icmp.NewProtocol6,
    }
    // ...
}
```

This patch introduces an alternative by enabling the stack to refrain from directly responding to ICMPv4 Echo packets delivered locally due to promiscuous mode, thereby allowing the defaultHandler to [handle](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/nic.go#L873) them independently.

**This proposal is presented as an initial step for discussion, and insights from experts on potential refinements or superior alternatives are warmly welcomed.**

**Testing and adjustments for ICMPv6 will be addressed once the approach is finalized.**

## Patch Details

In [ipv4.go:handleValidatedPacket](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/ipv4.go#L1108), the packet is evaluated based on `AcquireAssignedAddress` to determine local delivery or forwarding:

```go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

In promiscuous mode, packets destined for unknown addresses are assigned a temporary address and delivered locally:

```go
/*
handleValidatedPacket
 \----AcquireAssignedAddress
       \----AcquireAssignedAddressOrMatching
             \----addAndAcquireAddressLocked
*/
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB}, Temporary)
    // ...
}
```

By leveraging the `Temporary` field in [AddressProperties](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/stack/registration.go#L492), we can identify packets delivered locally due to promiscuous mode. A new field, `LocalAddressTemporary`, is added to `NetworkPacketInfo` to record this status:

```go
// pkg/tcpip/stack/addressable_endpoint_state.go
func (a *AddressableEndpointState) AcquireAssignedAddressOrMatching(localAddr tcpip.Address, f func(AddressEndpoint) bool, allowTemp bool, tempPEB PrimaryEndpointBehavior, readOnly bool) AddressEndpoint {
    // ...
    if !allowTemp { // e.nic.Promiscuous()
        return nil
    }
    // ...
    ep, err := a.addAndAcquireAddressLocked(addr, AddressProperties{PEB: tempPEB, Temporary: true}, Temporary) // set AddressProperties.Temporary
    // ...
}

// pkg/tcpip/network/ipv4/ipv4.go
func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) {
    // ...
    if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint, true /* readOnly */); addressEndpoint != nil {
        pkt.NetworkPacketInfo.LocalAddressTemporary = addressEndpoint.Temporary() // packets delivered locally due to promiscuous mode
        subnet := addressEndpoint.AddressWithPrefix().Subnet()
        pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast
        e.deliverPacketLocally(h, pkt, inNICName)
    } else if e.Forwarding() {
        e.handleForwardingError(e.forwardUnicastPacket(pkt))
    } else {
        stats.ip.InvalidDestinationAddressesReceived.Increment()
    }
}
```

Finally, in [icmp.go:handleICMP](https://github.com/google/gvisor/blob/6b2bcc44e061f48c6dd9cc6048ae17d389a2f22d/pkg/tcpip/network/ipv4/icmp.go#L282), direct replies to such packets are skipped:

```go
func (e *endpoint) handleICMP(pkt *stack.PacketBuffer) {
    // ...
    switch h.Type() {
    case header.ICMPv4Echo:
        // ...
        localAddressTemporary := pkt.NetworkPacketInfo.LocalAddressTemporary
        localAddressBroadcast := pkt.NetworkPacketInfo.LocalAddressBroadcast

        // It's possible that a raw socket or custom defaultHandler expects to
	// receive this packet.
        e.dispatcher.DeliverTransportPacket(header.ICMPv4ProtocolNumber, pkt)
        pkt = nil

        // Skip direct ICMP echo reply if the packet was received with a temporary
	// address, allowing custom handlers to take over.
        if localAddressTemporary {
            return
        }
        // ...
    }
}
```

## Quick Testing

To validate this patch, a simple test program and procedure are provided below:

```go
package main

import (
    "fmt"
    "net"
    "gvisor.dev/gvisor/pkg/tcpip"
    "gvisor.dev/gvisor/pkg/tcpip/header"
    "gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
    "gvisor.dev/gvisor/pkg/tcpip/link/tun"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
    "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
    "gvisor.dev/gvisor/pkg/tcpip/stack"
    "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
    "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)

func main() {
    fd, _ := tun.Open("tun0")
    ep, _ := fdbased.New(&fdbased.Options{
        FDs:                   []int{fd},
        MTU:                   1500,
        EthernetHeader:        false,
        PacketDispatchMode:    fdbased.Readv,
        MaxSyscallHeaderBytes: 0x00,
    })
    s := stack.New(stack.Options{
        NetworkProtocols: []stack.NetworkProtocolFactory{
            ipv4.NewProtocol,
            ipv6.NewProtocol,
        },
        TransportProtocols: []stack.TransportProtocolFactory{
            tcp.NewProtocol,
            udp.NewProtocol,
            icmp.NewProtocol4,
            icmp.NewProtocol6,
        },
    })

    nicID := s.NextNICID()
    s.CreateNICWithOptions(nicID, ep,
        stack.NICOptions{
            Disabled: false,
            QDisc:    nil,
        },
    )
    s.SetPromiscuousMode(nicID, true)
    s.SetSpoofing(nicID, true)
    s.SetRouteTable([]tcpip.Route{
        {Destination: header.IPv4EmptySubnet, NIC: nicID},
        {Destination: header.IPv6EmptySubnet, NIC: nicID},
    })
    s.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
        h := header.ICMPv4(pkt.TransportHeader().Slice())
        fmt.Printf("icmpv4: %s->%s, hType: %v, ", id.RemoteAddress, id.LocalAddress, h.Type())
        if !pkt.NetworkPacketInfo.LocalAddressTemporary {
            fmt.Println("packet to permanent address, processed by stack")
            return true
        }
        fmt.Println("packet to temporary address, need to process by user")
        return true
    })
    protocolAddr := tcpip.ProtocolAddress{
        Protocol: ipv4.ProtocolNumber,
        AddressWithPrefix: tcpip.AddressWithPrefix{
            Address:   tcpip.AddrFromSlice(net.IPv4(11, 0, 0, 1).To4()),
            PrefixLen: 8,
        },
    }
    s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint, Temporary: false})

    fmt.Println("stack started ...")
    select {}
}
```

Test procedure:

```shell
$ ip tuntap add mode tun dev tun0
ip link set dev tun0 up

$ go run main.go
stack started ...

icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack
icmpv4: 10.161.22.19->11.0.0.1, hType: 8, packet to permanent address, processed by stack

icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user
icmpv4: 10.161.22.19->11.0.0.2, hType: 8, packet to temporary address, need to process by user

```

```shell
$ ping 11.0.0.1
PING 11.0.0.1 (11.0.0.1) 56(84) bytes of data.
64 bytes from 11.0.0.1: icmp_seq=1 ttl=64 time=0.441 ms
64 bytes from 11.0.0.1: icmp_seq=2 ttl=64 time=0.425 ms
64 bytes from 11.0.0.1: icmp_seq=3 ttl=64 time=0.437 ms
^C
--- 11.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2012ms
rtt min/avg/max/mdev = 0.425/0.434/0.441/0.006 ms

$ ping 11.0.0.2
PING 11.0.0.2 (11.0.0.2) 56(84) bytes of data.
^C
--- 11.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2052ms
```

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11609 from Amaindex:master 868dfbc
PiperOrigin-RevId: 753217427
@copybara-service copybara-service bot merged commit 8090bc4 into google:master Apr 30, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: networking Issue related to networking ready to pull
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants